/** 
  Copyright (c) 2008, 2009 Claudio Imbrenda
  This program is released under the terms of the
  GNU GPL version 3 or later.
*/
package macchina;

import java.io.*;
import java.util.*;

public class Assemblatore {
  
  public static final String ABOUTSTRING=
          "Assembler for Macchina Ridotta\n"+
          " Copyright (c) 2008 Claudio Imbrenda,\n"+
          " all rights reserved.\n" +
          " Released under the GNU GPL license\n"+
          " version 3 or later.\n";
  
  /** Reader used to parse the source
   */
  private Reader sc;
    
  /** Macchina used to get instructions and opcodes based on its configuration.
   */
  private Macchina m;
  
  /** Log of the process. May be requested by invokers of Assemble().
   */
  String alog="";
  
  /** Creates a new instance of Assemblatore. 
   * @param f the file containig the source.
   * @param m an instance of Macchina, used for configurations.
   * @throws FileNotFoundException if the input file is not found.
   */
  public Assemblatore(File f,Macchina ma)throws FileNotFoundException{
    sc=new FileReader(f);
    m=ma;
  }  
    
  /** Creates a new instance of Assemblatore.
   * @param s a string containig the source.
   * @param m an instance of Macchina, used for configurations.
   */  
  public Assemblatore(String s,Macchina ma){
    sc =new StringReader(s);
    m=ma;
  }
  
  
  /** Returns and clears the log of the assembling process.
   * @return the log of the assembling process since the last invocation.
   */
  public synchronized String getLog(){
    String res=alog;
    alog="";
    return res;
  }
    
  /**
   * Appends to the log. 
   * @param s the string to be appended
   */
  private synchronized void appendLog(String s){
    alog=alog+"\n"+s;
  }
  
  /** Perform assemblation, output is written on a tape.
   * @param r a RegistroUscita, for writing to a tape.
   * @throws SyntaxErrorException in case of syntax errors.
   * @throws IOException in case of IO error reading the source.
   */
  public void assemble(RegistroUscita r)
                          throws SyntaxErrorException,IOException{
    short address=0;
    Pair<String,Integer> nn;
    
    ArrayList<Pair<Short,Integer>> numeri = 
                          new ArrayList<Pair<Short,Integer>>();
    ArrayList<Pair<Short,ArrayList<Short>>> istruzioni =
                          new ArrayList<Pair<Short,ArrayList<Short>>>();
    
    Pair<Short,ArrayList<Short>>curblock=
        new Pair<Short,ArrayList<Short>>((short)0,new ArrayList<Short>());
    
    Scanner s = new Scanner(sc);
    
    appendLog(ABOUTSTRING);
   
    while((nn = getnext(s))!=null){
      if(nn.a.equals("#"))continue;
      
      if(nn.a.equalsIgnoreCase(".org")){
        if((nn.b<0)||(nn.b>1023))error("Invalid address: "+nn.b);
        address=nn.b.shortValue();
        if(curblock.b.size()==0)
          curblock.a=nn.b.shortValue();
        else {
          istruzioni.add(curblock);
          curblock=new Pair<Short,ArrayList<Short>>(
                              nn.b.shortValue(), new ArrayList<Short>());
        }
        continue;
      }
      
      if(nn.a.equalsIgnoreCase(".word")){
        if((nn.b>=0)&&(nn.b<32768)&&((nn.b&0x3FF)<1000))
          curblock.b.add(nn.b.shortValue());
        else{
          numeri.add(new Pair<Short,Integer>(address,nn.b));
          curblock.b.add((short)0);
        }
        address++;
        continue;
      }
      
      try{
        byte opc = m.stringToOpcode(nn.a);
        if((nn.b<0)||(nn.b>=1000))
            error("Number "+nn.b+" is too big!");
        curblock.b.add((short)((opc<<10)|nn.b));
        address++;
      }catch(IllegalArgumentException e){
        error("Invalid instruction: "+nn.a);
      }
    }
    
    istruzioni.add(curblock);
            
    for(Pair<Short,ArrayList<Short>> l:istruzioni){
      
      int sz=l.b.size();
      if(sz>999)error("Program too long!");
      
      r.write((byte)31); //LTRS
      r.write(MRUtils.toMR((char)('0'+l.a/100)));
      r.write(MRUtils.toMR((char)('0'+(l.a%100)/10)));
      r.write(MRUtils.toMR((char)('0'+(l.a%10))));
      
      r.write(MRUtils.toMR((char)('0'+sz/100)));
      r.write(MRUtils.toMR((char)('0'+(sz%100)/10)));
      r.write(MRUtils.toMR((char)('0'+(sz%10))));
      
      for(Short cistruz:l.b){
        short ii=cistruz;
        r.write((byte)((ii>>10)&0x1F));
        r.write((byte)((ii%1000)/100));
        r.write((byte)((ii%100)/10));
        r.write((byte)(ii%10));
      }
    }
    
    for(Pair<Short,Integer>val:numeri){
      short addr=val.a;
      int num=val.b;
      r.write((byte)27); //CFRS
      r.write(MRUtils.toMR((char)('0'+addr/100)));
      r.write(MRUtils.toMR((char)('0'+(addr%100)/10)));
      r.write(MRUtils.toMR((char)('0'+addr%10)));
      r.write(MRUtils.toMR('0'));
      r.write(MRUtils.toMR('0'));
      r.write(MRUtils.toMR('1'));    
      r.write(val.b<0?MRUtils.toMR('-'):MRUtils.toMR('+'));
      val.b=Math.abs(val.b);
      r.write(MRUtils.toMR((char)('0'+(val.b%100000)/10000)));
      r.write(MRUtils.toMR((char)('0'+(val.b%10000)/1000)));
      r.write(MRUtils.toMR((char)('0'+(val.b%1000)/100)));
      r.write(MRUtils.toMR((char)('0'+(val.b%100)/10)));
      r.write(MRUtils.toMR((char)('0'+(val.b%10))));  
    }
    
  }
  
  /** Perform assemblation, output is raw.
   * WARNING: the output is written sequentially, ignoring any .org directive.
   * @param o an OutputStream to write the raw binary.
   *  Each memory cell is represented by a 4-byte little endian sequence.
   * @throws SyntaxErrorException in case of syntax errors.
   * @throws IOException in case of IO error reading the source.
   */
  public void assemble(OutputStream o)
                          throws SyntaxErrorException,IOException{
    
    int address=0;
    Pair<String,Integer> nn;
    
    Scanner s=new Scanner(sc);
    
    appendLog(ABOUTSTRING);
    
    while((nn = getnext(s))!=null){
      if(nn.a.equals("#"))continue;
      
      if(nn.a.equalsIgnoreCase(".org")){
        if((nn.b<0)||(nn.b>1023))error("Invalid address: "+nn.b);
        address=nn.b;
        continue;
      }
      
      if(nn.a.equalsIgnoreCase(".word")){
        if((nn.b>262143/* 2^18-1 */)||(nn.b<-131072/* -2^17 */))
          error("Number "+nn.b+" is too big!");
        MRUtils.writeword(o,nn.b);
        address++;
        continue;
      }
      
      try{
        byte opc = m.stringToOpcode(nn.a);
        if((nn.b<0)||(nn.b>1023))
            error("Number "+nn.b+"is too big!");
        MRUtils.writeword(o,nn.b|(opc<<10));
        address++;
      }catch(IllegalArgumentException e){
        error("Invalid instruction: "+nn.a);
      }
    }  
  }
  
  /**
   * Handles an error. Appends the string to the log and then throws a 
   * SyntaxErrorException containing the string.
   * @param s the description of the error condition.
   * @throws macchina.SyntaxErrorException
   */
  private void error(String s)throws SyntaxErrorException{
    appendLog(s);
    throw new SyntaxErrorException(s);
  }

  /** Reads the next token from the stream.
   * @param s the scanner to read from
   * @return a Pair containing the name of the instruction and the address.
   *  If no address is specified in the instruction, 0 is assumed.
   * @throws SyntaxErrorException if a syntax error occours.
   */
  private Pair<String,Integer> getnext(Scanner s)throws SyntaxErrorException{
    Pair<String,Integer> res= new Pair<String,Integer>("",0);
    String resto;
    
    if(!s.hasNext())return null;
    res.a=s.next().toLowerCase();
    
    if(res.a.startsWith("#")||res.a.startsWith(";")){
      res.a="#";
      if(s.hasNextLine())resto=s.nextLine();
      else while(s.hasNext())resto=s.next();
      return res;
    }
    
    if(s.hasNextInt()){res.b=s.nextInt();}
    else if(s.hasNextFloat()&&res.a.equalsIgnoreCase(".word"))
              res.b=ftoint(s.nextFloat());
    
    if(s.hasNextLine())resto=s.nextLine();
    else resto=s.next(); //TODO: if file has no newline at the end then crash!
    
    resto=resto.trim();
    if(!((resto.length()==0)||resto.startsWith(";")||resto.startsWith("#")))
          error("Garbage at the end of the line: \""+resto+"\" "+
                      " (in line containing: "+res+")");
    
    return res;
  }
  
  /**
   * Converts a floating point value to a 18-bit fixed point value, as used
   * by the Macchina Ridotta.
   * @param f the floating point value to convert
   * @return an int containing the 18-bit fixed point representation of the
   * given number.
   * @throws macchina.SyntaxErrorException if the number is too big to be 
   * represented for the limited 18-bit signed -1..1 range.
   */
  private int ftoint(float f)throws SyntaxErrorException{
    if((f<-1)||(f>=1))error("Number "+f+" is too big!");
    return (int)(f*(1<<18));
  }
  
}
